<?php

/**
 * @package    sfDoctrineCronManagerPlugin
 * @author     Bob Stremel <bob.stremel@gmail.com>
 */
abstract class CronManager
{
  //  Associative arrays of cron jobs
  protected $jobs = array();
  protected $current_load; //  one minute average
  protected $now;
  //  Individual settings for a particular cron job, used to save jobs
  protected $job_record;

  public function __construct()
  {
    //  Windows work-around for load average
    $load = array(0);
    if(function_exists('sys_getloadavg'))
    {
      $load = sys_getloadavg();
    }
    $this->current_load = $load[0];
  }

  abstract public function loadJobs();
  abstract public function saveJob();
  abstract public function runProcessOnce($cron_id);
  abstract public function updateRunData($job);
  public function setJobData($key, $value)
  {
    $this->job_record[$key] = $value;
  }

  public function execute()
  {
    $this->loadJobs();
    $this->now = getdate();

    foreach($this->jobs as $job)
    {
      //  Begin operation, check to see if current job was missed
      $nextRun = strtotime($job['next_run']);
      if($nextRun < time() - 59)
      {
        //  If we missed our last run and the job is active, run
        if($job['run_if_missed'] and $job['active'])
        {
          $this->runProcess($job);
        }
        else
        {
          //  This job only runs at its designated time, check to see if we run based on the current time
          $this->execute_on_time($job);
        }
      }
      else
      {
        //  We did not miss our last run, check to see if we run based on the current time
        $this->execute_on_time($job);
      }
    }
  }

  protected function execute_on_time($job)
  {
    //  If the job is active...
    if($job['active'])
    {
      //  ...check the times
	  $run = $this->isReadyByTime($job, $this->now);

      if($run)
      {
        //  Check load limit
        $load_limit = $job['load_limit'];
        if($load_limit <= $this->current_load and $load_limit != sfConfig::get('app_sfCronManagerPlugin_load_limit_ignore', -1))
        {
          //  Do not run, over the load limit.  Suppress the job
          $suppressed = $job['current_suppressed_runs'];
          $suppression_limit = $job['max_suppressed_runs'];
          if($suppressed >= $suppression_limit)
          {
            //  Run it anyway and reset the counter
            $run = TRUE;
            $job->resetSuppressedRuns();
          }
          else
          {
            //  Run has been suppressed
            $run = FALSE;
            $job->incrementSuppressedRuns();
          }
        }

        if($run)
        {
          //  OK to run.  Perform the operation.
          $this->runProcess($job);
        }
      }
    }
  }

  public function isReadyByTime($job, $now)
  {
      //  OK to run based on minutes setting?
      $run = $this->validToRun($now['minutes'], $job['minute'], 0, 59);

      //  OK to run based on hours setting?
      $run = $run && $this->validToRun($now['hours'], $job['hour'], 0, 23);

      //  OK to run based on day of month setting?
      $run = $run && $this->validToRun($now['mday'], $job['day_of_month'], 1, 31);

      //  OK to run based on month setting?
      $run = $run && $this->validToRun($now['mon'], $job['month'], 1, 12);

      //  OK to run based on day of week setting?
      $run = $run && $this->validToRun($now['wday'], $job['day_of_week'], 0, 7);

	  return $run;
  }

  protected function runProcess($job)
  {
    //  Run it
    $command = $job['command'];

    if(!empty($command))
    {
      //  run command
      shell_exec($command);

      //  update the jobs' run times
      $this->updateRunData($job);
    }
  }

  public function getNextRunDate($job, $now = null)
  {
    if(empty($now) or !is_array($now))
    {
      $now = getdate();
    }

    $minutes = $this->getRunTimes($now['minutes'], $job['minute'], 0, 59);
    $hours = $this->getRunTimes($now['hours'], $job['hour'], 0, 23);
    $DOMs = $this->getRunTimes($now['mday'], $job['day_of_month'], 1, 31);
    $months = $this->getRunTimes($now['mon'], $job['month'], 1, 12);
    $DOWs = $this->getRunTimes($now['wday'], $job['day_of_week'], 0, 7);

    //  Create a month matrix for the next year
    $monthOffset = 0;
    $yearOffset = 0;
    $matrix = array();
    for($monthIndex = 0; $monthIndex <= 13; $monthIndex++)
    {
      $mo = $now['mon'] + $monthIndex + $monthOffset;
      if($mo > 12)
      {
        $monthOffset = -12;
        $yearOffset = 1;
      }

      if(!in_array($mo, $months))
      {
        continue;
      }

      foreach($DOMs as $dom)
      {
        if(checkdate($mo, $dom, $now['year'] + $yearOffset))
        {
          $matrix[] = mktime(0, 0, 0, $mo, $dom, $now['year'] + $yearOffset);
        }
      }
    }

    //  Remove days of the week that don't match
    $ct = count($matrix);
    for($i = 0; $i < $ct; $i++)
    {
      $remove = true;
      foreach($DOWs as $dow)
      {
        if($dow == 7)
        {
          $dow = 0;
        }
        $testdate = getdate($matrix[$i]);
        if($testdate['wday'] == $dow)
        {
          $remove = false;
          break;
        }
      }
      if($remove)
      {
        unset($matrix[$i]);
      }
    }

    //  No matches?
    if(count($matrix) == 0)
    {
      return date("Y-m-d H:i:s", sfConfig::get('app_sfCronManagerPlugin_timestamp_max', 2147483647));
    }

    //  Re-index the matrix
    $matrix = array_merge($matrix);

    //  minutes
    $minct = count($minutes);
    $theMinute = $minutes[0];
    $hourRollover = true;
    for($i = 0; $i < $minct; $i++)
    {
      if($minutes[$i] > $now['minutes'])
      {
        $hourRollover = false;
        $theMinute = $minutes[$i];
        break;
      }
    }

    //  hours
    $ct = count($hours);
    $theHour = $hours[0];
    $domRollover = true;
    $noCondition = true;
    for($i = 0; $i < $ct; $i++)
    {
      if($hours[$i] >= $now['hours'])
      {
        $noCondition = false;
        //  Different hour, same day
        if($hours[$i] > $now['hours'])
        {
          $theMinute = $minutes[0];
        }

        if($hourRollover)
        {
          $theMinute = $minutes[0];
          if(isset($hours[$i + 1]))
          {
            $theHour = $hours[$i + 1];
            $domRollover = false;
          }
          else
          {
            $theHour = $hours[0];
          }
        }
        else
        {
          $theHour = $hours[$i];
          $domRollover = false;
        }
        break;
      }
    }
    if($noCondition)
    {
      //  We didn't meet the condition, so we have an hour rollover
      $theMinute = $minutes[0];
    }

    //  Find the next datetime in the matrix after now
    $ct = count($matrix);
    $todayIndex = 0;
    for($i = 0; $i < $ct; $i++)
    {
      if((date('z', $matrix[$i]) >= $now['yday']))
      {
        $todayIndex = $i;
        if($domRollover and date('z', $matrix[$i]) == $now['yday'])
        {
          $todayIndex++;
        }
        break;
      }
    }

    //  If that datetime is not today, hour and minute go to default settings
    if(date('z', $matrix[$todayIndex]) != $now['yday'])
    {
      $theHour = $hours[0];
      $theMinute = $minutes[0];
    }

    return date("Y-m-d H:i:s", $matrix[$todayIndex] + ($theMinute * 60) + (($theHour) * 3600));
  }

  protected function validToRun($now, $cronString, $start, $end)
  {
	$run_times = $this->getRunTimes($now, $cronString, $start, $end);
	$valid = in_array($now, $run_times);
    return $valid;
  }

  protected function getRunTimes($now, $cronString, $start, $end)
  {
    $times = $this->expand($cronString, $start, $end);
    return $times;
  }

  public function expand($values, $start, $end)
  {
    $newArray = array();

    //	Step 1 - explode by commas
    $values = explode(',', $values);

    foreach($values as $value)
    {
      //  Step 2 - explode comma-separated values by dashes
      $dashes = explode('-', $value);
      if(count($dashes) == 2)
      {
        $start_dashes = $dashes[0];
        $end_dashes = $dashes[1];

        //	Step 3 - check for a slash
        $slashes = explode('/', $dashes[1]);
        $step_size = 0;
        if(count($slashes) == 2)
        {
          $end_dashes = $slashes[0];
          $step_size = $slashes[1];
        }
		$offset = $start_dashes; // so the modulus function will work
        for($i = $start_dashes; $i <= $end_dashes; $i++)
        {
          if($step_size > 0)
          {
            if(($i - $offset) % $step_size == 0)
            {
              $newArray[] = $i;
            }
          }
          else
          {
            $newArray[] = $i;
          }
        }
      }
      else if(strpos($value, '*') === 0)
      {
        $slashes = explode('/', $value);
        if(count($slashes) == 2)
        {
          $step_size = $slashes[1];
          for($i = $start; $i <= $end; $i++)
          {
            if($i % $step_size == 0)
            {
              $newArray[] = $i;
            }
          }
        }
        else
        {
          for($i = $start; $i <= $end; $i++)
          {
            $newArray[] = $i;
          }
        }
      }
      else
      {
        $newArray[] = $value;
      }
    }
	$newArray = array_unique($newArray);
	sort($newArray);
    return $newArray;
  }

  protected function limitBySlashes($values, $step_size)
  {
    if(empty($values))
    {
      return $values;
    }

    $current = 0;
    foreach($values as $idx => $value)
    {
      if($current == 0)
      {
        $current = $step_size;
      }
      else
      {
        unset($values[$idx]);
      }
      $current--;
    }
    $values = array_merge($values);
    return $values;
  }

}